package com.hyacinth.dubbo.io.netty.client;

import com.hyacinth.dubbo.io.netty.client.console.ConsoleCommandManager;
import com.hyacinth.dubbo.io.netty.client.console.LoginConsoleCommand;
import com.hyacinth.dubbo.io.netty.client.handler.*;
import com.hyacinth.dubbo.io.netty.codec.PacketCodecHandler;
import com.hyacinth.dubbo.io.netty.codec.PacketDecoder;
import com.hyacinth.dubbo.io.netty.codec.PacketEncoder;
import com.hyacinth.dubbo.io.netty.codec.Spliter;
import com.hyacinth.dubbo.io.netty.monitor.IMIdleStateHandler;
import com.hyacinth.dubbo.io.netty.util.SessionUtil;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.Future;

import java.util.Date;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

/**
 * netty客户端
 * @author zhangfb
 * @version 1.0.0.1
 * @since JDK 1.8
 */
public class NettyClient {

    private static final int MAX_RETRY = 5;
    private static final String HOST = "127.0.0.1";
    private static final int PORT = 8080;

    public static void main(String[] args) {
        // 创建工作线程组
        NioEventLoopGroup workGroup = new NioEventLoopGroup();

        // 创建客户端启动引导
        Bootstrap bootstrap = new Bootstrap();
        bootstrap
                // 1.指定线程模型
                .group(workGroup)
                // 2.指定 IO 类型为 NIO
                .channel(NioSocketChannel.class)
                // 设置相关属性
                //连接的超时时间，超过这个时间还是建立不上的话则代表连接失败
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                //是否开启 TCP 底层心跳机制，true 为开启
                .option(ChannelOption.SO_KEEPALIVE, true)
                // 是否开始 Nagle 算法，true 表示关闭，false 表示开启，
                // 通俗地说，如果要求高实时性，有数据发送时就马上发送，就设置为 true 关闭，
                // 如果需要减少发送次数减少网络交互，就设置为false开启
                .option(ChannelOption.TCP_NODELAY, true)
                // 3.IO 处理逻辑
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        // 空闲检测
                        ch.pipeline().addLast(new IMIdleStateHandler());
                        ch.pipeline().addLast(new Spliter());
                        ch.pipeline().addLast(new PacketDecoder());
                        // 登录响应处理器
                        ch.pipeline().addLast(new LoginResponseHandler());
                        // 收消息处理器
                        ch.pipeline().addLast(new MessageResponseHandler());
                        // 创建群响应处理器
                        ch.pipeline().addLast(new CreateGroupResponseHandler());
                        // 加群响应处理器
                        ch.pipeline().addLast(new JoinGroupResponseHandler());
                        // 退群响应处理器
                        ch.pipeline().addLast(new QuitGroupResponseHandler());
                        // 获取群成员响应处理器
                        ch.pipeline().addLast(new ListGroupMembersResponseHandler());
                        // 群消息响应处理
                        ch.pipeline().addLast(new GroupMessageResponseHandler());
                        // 登出响应处理器
                        ch.pipeline().addLast(new LogoutResponseHandler());
                        ch.pipeline().addLast(new PacketEncoder());
                        // 心跳定时器
                        ch.pipeline().addLast(new HeartBeatTimerHandler());
                    }
                });
        // 4.建立连接
        connect(bootstrap, HOST, PORT, MAX_RETRY);
    }

    /**
     * 实现指数退避的客户端重连
     * 退避算法就是网络上的节点在发送数据冲突后，等待一定时间后再发，等待时间是随指数增长，从而避免频繁的触发冲突。
     * 在计算机网络中，二进制指数退避算法或截断指数退避算法常常作为避免网络堵塞的一部分用于同一数据块的重发策略。
     * @param bootstrap 引导
     * @param host      主机名
     * @param port      端口
     * @param retry     重试次数
     */
    private static void connect(Bootstrap bootstrap, String host, int port, int retry) {
        bootstrap.connect(host, port).addListener((Future<? super Void> future) -> {
            if (future.isSuccess()) {
                System.out.println(new Date() + ": 连接成功，启动控制台线程……");
                Channel channel = ((ChannelFuture) future).channel();
                startConsoleThread(channel);
            } else if (retry == 0) {
                System.err.println("重试次数已用完，放弃连接!");
            } else {
                // 第几次重连
                int order = (MAX_RETRY - retry) + 1;
                // 本次重连的间隔，指数退避
                int delay = 1 << order;
                System.err.println(new Date() + ": 连接失败，第" + order + "次重连……");
                // 定期重连
                bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry -1), delay, TimeUnit.SECONDS);
            }
        });
    }

    /**
     * 开启控制台键入线程
     * @param channel 通道
     */
    private static void startConsoleThread(Channel channel) {
        ConsoleCommandManager consoleCommandManager = new ConsoleCommandManager();
        LoginConsoleCommand loginConsoleCommand = new LoginConsoleCommand();
        Scanner scanner = new Scanner(System.in);

        new Thread(() -> {
            while (!Thread.interrupted()) {
                if (!SessionUtil.hasLogin(channel)) {
                    loginConsoleCommand.exec(scanner, channel);
                } else {
                    consoleCommandManager.exec(scanner, channel);
                }
            }
        }).start();
    }
}
